查看原文
其他

OpenCV的机器视觉图像处理技术实现之Core模块

2017-09-29 异步图书
点击标题下「异步图书」可快速关注            

影像处理早期是从扫描的文件中识别出文字(OCR),后来才发展为手写识别、自拍修图等静态图像处理。机器人技术的应用从早期的组装自动化,到中期的生产质量监控,再到近期逐渐走进人群的应用,整个发展过程中都涉及图像处理的技术。因此,图像处理不仅会更加流行,而且会更加普及和接近大众。

Core 模块是OpenCV最基本的模块,因为所有OpenCV需要的数据结构与基本的绘图功能都在Core模块内,所以在项目中,Core也是第一个一定要具备的模块。这些基本数据结构与绘图功能,请参考OpenCV说明文档的网址http://docs.opencv.org/。本文将以程序示例来介绍其实际应用,因为任何图书都不可能完全介绍OpenCV的所有功能,所以读者要养成经常查看这些说明文档的习惯。只要单击网页内的“core. The Core Functionality”就可以查看Core模块。要查看其他模块,方法也是相同的。

2.1 显示图文件

因为OpenCV有太多模块,而每个模块又有许多函数,本书就以程序示例的方式来介绍。在程序中使用到的函数,我们才会给出说明,其他未说明的部分,请读者自行查阅OpenCV的说明文档。

至此,项目终于设置完成,现在我们开始编写程序。因为我们只是学习操作,所以先剪切并粘贴代码比较简单,其方式如下:

在右侧“Solution Explorer”中的“Source File”中单击鼠标右键,选择“Add”再选择“New Item”来加入代码,如图2-1所示。

图2-1

输入程序文件名称,这里使用与项目相同的名称,如图2-2所示。

图2-2

本小节所介绍的代码设置,与其他项目使用的方式都相同,后面所有的项目按照此设置,不再重复说明。

继续先前项目,设置完成有了程序文件之后,就粘贴复制的代码,如图2-3所示。

图2-3

粘贴程序后再单击选单中绿色的三角形按钮,如图2-4所示。

图2-4

Visual Studio会询问是否进行编译,单击“Yes”按钮开始执行,如图2-5所示。

执行本程序之前,我们应该先将本书所使用的图文件都复制到C:\images文件夹下。如果图文件位于不同的目录,请自行修改程序对应的文件夹。

最后程序结果显示,如图2-6所示。

图2-5

图2-6

本项目只有一个程序文件。如果项目有许多C++程序与include文件,请全部复制到这个项目内,如图2-7所示。

要注意,就此范例而言,文件应该复制到Display_Image/Display_Image文件夹下。

图2-7

复制完成后再分别将复制的文件加入项目内。加载Include文件的方法与加载C++程序文件的做法一样,只是Include文件要加载Solution Explorer的Header File,C++文件要加载Solution Explorer的Source File。

现在以加入C++程序文件来做说明。在“Solution Explorer”的“Source File”上单击鼠标右键,再依次单击“Add”与“Existing item...”,如图2-8所示。

图2-8

Visual Studio就会弹出窗口让你选择要加入的内容,如图2-9所示。

图2-9

这只是OpenCV的“Hello World”(第一个测试程序的意思),所以直接写入要处理的文件(本书范例全都是直接写入的状态)。读者也可以自行与MFC或C++/CLI整合来处理UI问题,这部分不是本书探讨的范畴。

原始文件都是以argv作为输入文件,可以改变文件名,或在项目属性的“Debugging”选项中设置,如图2-10所示。笔者认为直接写入程序并且印在书中,读者在阅读时则能看得清程序的结构。

图2-10

注意

因为Windows 7在复制文件路径时会自动变成反斜杠,所以这里继续沿用,如图2-11所示。为了避免C++程序编译错误,所以改成双反斜杠,如果使用正斜杠就不需要再使用双正斜杠了,如图2-12所示。

图2-11

图2-12

程序内容如下:

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include <iostream>using namespace cv;using namespace std;int main( int argc, char** argv ){   argv[1] = "C:\\images\\lena.jpg";   Mat image;   // 载入图文件   image = imread(argv[1], CV_LOAD_IMAGE_COLOR);   // 检查读取文件是否成功   if(! image.data )   {      cout <<  "无法打开或找不到图文件" << std::endl ;        return -1;   }   // 建立显示图文件窗口   namedWindow("Display window", CV_WINDOW_NORMAL);   // CV_WINDOW_FREERATIO 与 CV_WINDOW_KEEPRATIO   // CV_GUI_NORMAL 与 CV_GUI_EXPANDED   // 在窗口内显示图文件   imshow( "Display window", image );   // 窗口等待按键   waitKey(0);   return 0;}

程序说明

本书先假设读者已对C++有所接触认识,代码内容属于C++而不是OpenCV的范畴,在此不再赘述。

1.Mat image

Mat是OpenCV新定义的数据类型,类似传统的数据类型int、float或String。Mat这种数据类型表示图像,而图像都是二维数组。所以OpenCV就定义了处理图像的矩阵类别(Matrix),取英文的前3个字母Mat,就如同int取integer的前3个字母一样。

OpenCV常用的数据结构有以下几种。

  • Point:代表二维的点,用于图像的坐标点。

  Point pt;

  pt.x = 10;

  pt.y = 8;

或是:

   Point pt = Point(10, 8)

  • Scalar:代表4元素的向量(4-element vector),一般用于RGB颜色值。

   Scalar(a, b, c),第4个参数如果用不到可以省略。

   a代表蓝色值、b代表绿色值、c代表红色值,也就是Scalar(B,G,R)。

MAT类别一般运算的说明见表2-1。

表2-1

语  法

说  明

double m[2][2] = {{1.0, 2.0}, {3.0, 4.0}};
Mat M(2, 2, CV_32F, m);

由多维数组(array)产生2×2矩阵

Mat M(100, 100, CV_32FC2, Scalar(1, 3));

声明100×100的两个通道的矩阵,第一个通道值是1,第二个通道值是3

M.create(300, 300, CV_8UC(15));

产生300×300的15个通道的矩阵,之前变量M内的值将被删除

Mat ima(240, 320, CV_8U, Scalar(100))

产生240×320,单一通道(灰阶)的矩阵

int sizes[3] = {7, 8, 9};
Mat M(3, sizes, CV_8U, Scalar::all(0);

产生三维矩阵,各维的维度值(dimension)分别是7、8和9,矩阵的初始值是0

Mat M = Mat::eye(7, 7, CV_32F);

产生32位浮点(float)的7×7的矩阵

Mat M = Mat::zeros(7, 7, CV_54F);

产生64位浮点(float)的7×7的矩阵,矩阵初始值是0

M.at<double>(i, j)

取得矩阵M在第i行、第j列的值,该值的数据类型是double。
注意:行列的初始值都是0

M.row(1)

取得矩阵M在第1行的值

M.col(3)

取得矩阵M在第3列的值

M.rowRange(1, 4)

取得矩阵M在第1行到第4行的值

M.colRange(1, 4)

取得矩阵M在第1列到第4列的值

M.rowRange(2, 5).colRange(1, 3)

取得矩阵M在第2行到第5行及第1列到第3列的值

M.diag();

取得矩阵M的对角值

Mat M2 = M1.clone();

将M1复制给M2

Mat M2; M1.copyTo(M2);

将M1复制给M2

Mat M1 = Mat::zeros(9, 3, CV_32FC3);
Mat M2 = M2 = M1.reshape(0, 3);

声明3行的M2矩阵,其通道与M1相同(reshape的第一个参数0),3行的数组是reshape的第二个参数3

Mat M2 = M1.t();

M2是M1的转置(transpose)

Mat M2 = M1.inv();

M2是M1的逆矩阵(inverse)

Mat M3 = M1 * M2;

M3是M1乘M2(矩阵相乘)

Scalar s;
Mat M2 = M1 + s;

M2是M1矩阵加上s颜色值(scalar)

Mat image;
image.size().height
image.size().width

Image矩阵的高和宽

Mat是数据类型,就会被当成参数在函数之间传递。但是因为图文件可能非常庞大,为了节省内存与性能,OpenCV都是以传址的方式处理的,也就是共用一份内存。现在用下面的代码来说明这一点。

Mat A, C;
A = imread(argv[1], CV_LOAD_IMAGE_COLOR);
C = A;
Mat fun(A);

上面的程序声明了两个图像数据类型A和C,A的内容是读入的图文件,然后A又传递C(C = A),A再以参数传递给fun函数。如果fun函数对A做任何处理,可能改变了A,fun函数执行完C也会被改变,即使C不在fun函数的作用域之内。

不同数据类型名称如果共用内存的话,何时才会真的从内存消失?OpenCV是使用C++的引用计数(reference couting),所以我们在设计程序时不用考虑做内存管理,否则就要执行对象释放(Release)的操作,这就是OpenCV 2.0与 OpenCV 1.0的差别。

2.imread (const string& filename, int flags):读取图文件

filename:图文件名称

flags:读取的方式。本例是读取彩色的内容。

OpenCV可以读取的图文件种类如下。

  • BMP:Windows位图文件。

  • PBM、PGM、PPM:可移植图文件格式。

  • SR、RAS:Sun的图文件格式。

  • JPEG、JPG、JPE:JPEG图文件格式。

  • TIFF、TIF:TIFF图文件格式。

  • PNG:可移植网络图文件格式。

读取的方式有下列3种选项:可使用如下例程名称,或用数值表示。

  • CV_LOAD_IMAGE_UNCHAMGED:这是以图文件的原始文件形态(彩色或黑白)读取,使用数值是小于零的数值。

  • CV_LOAD_GRAYSCALE:这是以黑白方式读取,使用数值是等于零的数值。

  • CV_LOAD_COLOR:这是以彩色方式读取,使用数值是大于零的数值。

3.namedWindow (const string& winname, int flags):建立要显示图文件的窗口

winname:窗口名称。这也表示可以同时建立多个窗口,再以名称区别

flags有下列几种选择。

  • WINDOW_AUTOSIZE:这是窗口大小不可改变,会按照图文件的大小自动调整;如果使用CV_WINDOW_AUTOSIZE也可以达到同样效果,这是OpenCV 1.0的用法

  • WINDOW_NORMAL:这是窗口大小可以改变,但必须是使用Qt开发;有关使用C++与Qt的说明,请参考http://qt-project.org/wiki/OpenCV_with_Qt。

  • WINDOW_OPENGL:设置支持OpenGL时,就要使用此参数。

不过笔者现阶段测试失败了,但是在C:\OpenCV\sources\modules\highgui\src\window_ w32.cpp程序中查找OPENGL部分,系统的确是有支持OpenGL的功能,可能OpenCV在构建程序库时有参数设置遗漏了。 - CV_WINDOW_NORMAL:如果重新建立对 OpenGL 的支持,想要再次使用WINDOW_AUTOSIZE就会出错,此时需要使用此参数。这可以使窗口大小随意改变,放大不会导致图像失真,缩小也不会影响原图内容比例。

这个参数在没有支持OpenGL时也可以使用,该选项特别适用于窗口上有滑动杆,而滑动杆大于窗口宽度时,或图太小想放大窗口看清效果。

4.imshow(const string& winname, InputArray mat):显示窗口

winname:窗口名称,会显示在窗口的左上方,方便窗口之间的分辨或比较。

mat:图文件所声明的矩阵参数。

本程序示范显示图像窗口,当程序结束时窗口就会自动关闭。如果程序还有其他部分在使用中,但又想关闭窗口时,则可以使用destroyWindow()来关闭窗口,或使用destroyAllWindows()来关闭所有窗口。

5.waitKey():等待按键

这是等待用户按下任意键继续执行,否则程序会立刻结束,并且无法看到程序执行结果。其参数是以毫秒(ms)为单位的等待时间。如果参数值为零,则表示持续等待。

注意,这个等待功能只能用于imshow所显示的窗口,若没有任何使用imshow显示的窗口,那waitKey()将没有任何作用。此时可改为用传统的getchar()实现程序执行的暂停功能。

如果将鼠标光标在图像的窗口上移动,光标会自动变成十字形,这表示OpenCV在窗口处理上还可以处理鼠标。第11章的实际案例应用的图像采集就是鼠标光标的示例。

图像数据结构实例

学习OpenCV,除了函数之外,就是数据结构最重要了,所以我们再以程序范例来对此加以说明。不过一般图像矩阵是无法用 << 来显示内容的,本程序只是以读者看得懂的方式来说明。

#include "opencv2/core/core.hpp"#include <iostream>using namespace std;using namespace cv;int main(int, char**){   // 用构造函数建立数据   Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255));   cout << "M = " << endl << " " << M <<  endl;   // 用create函数建立数据   M.create(2, 2, CV_8UC(2));   cout << "M = " << endl << " " << M << endl;   // 建立多维矩阵   int sz[3] = { 2, 2, 2 };   Mat L(3, sz, CV_8UC(1), Scalar::all(0));   // 无法用 << 运算符输出   // 用MATLAB风格的眼建立数据   Mat E = Mat::eye(3, 3, CV_64F);   cout << "E = " << endl << " " << E << endl;   // 数据都是1   Mat O = Mat::ones(2, 2, CV_32F);   cout << "O = " << endl << " " << O << endl;   // 数据都是0   Mat Z = Mat::zeros(2, 2, CV_8UC1);   cout << "Z = " << endl << " " << Z << endl;   // 建立3x3双精确度矩阵,值由<<输入   Mat C = (Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);   cout << "C = " << endl << " " << C << << endl << endl;   // 复制第一行数据   Mat RowClone = C.row(1).clone();   cout << "RowClone = " << endl << " " << RowClone << endl;   // 以随机数值填入矩阵内   Mat R = Mat(3, 2, CV_8UC3);   randu(R, Scalar::all(0), Scalar::all(255));   // 展示各种输出格式选项   cout << "R (default) = " << endl << R << endl;   cout << "R (python)  = " << endl << format(R, "python") << endl;   cout << "R (numpy)   = " << endl << format(R, "numpy") << endl;   cout << "R (csv)     = " << endl << format(R, "csv") << endl;   cout << "R (c)       = " << endl << format(R, "C") << endl;   // 图像中二维的点   Point2f P(5, 1);   cout << "Point (2D) = " << P << endl << endl;   // 图像中三维的点   Point3f P3f(2, 6, 7);   cout << "Point (3D) = " << P3f << endl << endl;   vector<float> v;   v.push_back((float)CV_PI);    v.push_back(2);    v.push_back(3.01f);   cout << "浮点向量矩阵 = " << Mat(v) << endl << endl;   vector<Point2f> vPoints(5);   for (size_t i = 0; i < vPoints.size(); ++i)      vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));   cout << "二维图点向量 = " << vPoints << endl;   getchar();   return 0;}

执行结果如图2-13所示。

图2-13

下雪特效

下雪特效的代码如下:

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include <iostream>using namespace cv;using namespace std;int main(int argc, char** argv){   argv[1] = "C:\\images\\lake.jpg";   Mat image;   // 载入图文件   image = imread(argv[1], CV_LOAD_IMAGE_COLOR);   // 检查读取文件是否成功   if (!image.data)   {     cout << "无法打开或找不到图文件" << std::endl;     return -1;   }   // 建立显示图文件窗口   namedWindow("原图", CV_WINDOW_NORMAL);   namedWindow("下雪图", CV_WINDOW_NORMAL);   imshow("原图", image);   // 雪点数   int i = 600;   for (int k = 0; k < i; k++) {      // rand() is the MFC random number generator      // try qrand() with Qt      int i = rand() % image.cols;      int j = rand() % image.rows;      if (image.channels() == 1) { // gray-level image         image.at<uchar>(j, i) = 255;         if (i < (int)image.cols)            image.at<uchar>(j + 1, i) = 255;         if (j < (int)image.rows)            image.at<uchar>(j, i + 1) = 255;         if (i < (int)image.cols && j < (int)image.rows)            image.at<uchar>(j + 1, i + 1) = 255;      }      else if (image.channels() == 3) { // color image         image.at<cv::Vec3b>(j, i)[0] = 255;         image.at<cv::Vec3b>(j, i)[1] = 255;         image.at<cv::Vec3b>(j, i)[2] = 255;         if (i < (int)image.cols - 1)         {            image.at<cv::Vec3b>(j, i + 1)[0] = 255;            image.at<cv::Vec3b>(j, i + 1)[1] = 255;            image.at<cv::Vec3b>(j, i + 1)[2] = 255;         }         if (j < (int)image.rows - 1)         {            image.at<cv::Vec3b>(j + 1, i)[0] = 255;            image.at<cv::Vec3b>(j + 1, i)[1] = 255;            image.at<cv::Vec3b>(j + 1, i)[2] = 255;         }         if (j < (int)image.rows - 1 && i < (int)image.cols - 1)         {            image.at<cv::Vec3b>(j + 1, i + 1)[0] = 255;            image.at<cv::Vec3b>(j + 1, i + 1)[1] = 255;            image.at<cv::Vec3b>(j + 1, i + 1)[2] = 255;         }      }   }   // 在窗口内显示图文件   imshow("下雪图", image);   // 窗口等待按键   waitKey(0);   return 0;}

程序执行的结果对比,如图2-14所示。

(a)原图

(b)飘雪图

图2-14

本程序是示范图像矩阵内每一个点的处理。

2.2 图文件转换

图文件转换的代码如下

#include <opencv\cv.h>#include <opencv\highgui.h>using namespace cv;int main(int argc, char* argv){   // 图文件   char* imageName = "C:\\images\\lena.jpg";   // 读取图文件   Mat image = imread(imageName, 1);   Mat gray_image;   // 图文件从BGR转成灰度   cvtColor(image, gray_image, CV_BGR2GRAY);   // 存储转换后的图文件   imwrite("C:\\images\\process\\Gray_lena.jpg", gray_image);   // 显示图文件窗口大小的控制   namedWindow(imageName, CV_WINDOW_AUTOSIZE);   namedWindow("Gray image", CV_WINDOW_AUTOSIZE);   // 显示原先图文件   imshow(imageName, image);   // 显示灰度图文件   imshow("C:\\images\\process\\Gray image", gray_image);   waitKey(0);   return 0;}

程序说明

1.cvtColor (InputArray src, OutputArray dst, int code, int dstCn=0):图像颜色空间 (color space) 转换

(1)src:输入图像。

(2)dst:输出图像。

(3)code:颜色空间转换种类。

  • RGB与CIE转换:CV_BGR2XYZ、CV_RGB2XYZ、CV_XYZ2BGR、CV_XYZ2RGB。

  • RGB与YCrCB JPEG转换:CV_BGR2YCrCb、CV_RGB2YCrCb、CV_YCrCb2BGR、CV_YCrCb2RGB。

  • RGB与HSV转换:CV_BGR2HSV、CV_RGB2HSV、CV_HSV2BGR、CV_HSV2RGB。

  • RGB与HLS转换:CV_BGR2HLS、CV_RGB2HLS、CV_HLS2BGR、CV_HLS2RGB。

  • RGB与CIE Lab*转换:CV_BGR2Lab、CV_RGB2Lab、CV_Lab2BGR、CV_Lab2RGB。

  • RGB与CIE Luv转换:CV_BGR2Luv、CV_RGB2Luv、CV_Luv2BGR、CV_Luv2RGB。

  • Bayer转换成RGB:CV_BayerBG2BGR、CV_BayerGB2BGR、CV_BayerRG2BGR、CV_BayerGR2BGR、CV_BayerBG2RGB、CV_BayerGB2RGB、CV_BayerRG2RGB、CV_BayerGR2RGB。

(4)dstCn:输出图像通道数,如果值为0,输出图像通道数由输入图像src与颜色空间code自动取得。

2.imwrite(const string& filename, InputArray img):存储图像

(1)filename:要存储的文件名。

(2)img:要存储的图像。

程序执行结果,图2-15(a)为原图彩色,图2-15(b)为转换后的灰度图。

(a)原图彩色

(b)灰度图

图2-15

2.3 图文件混合

图文件混合的代码如下:

#include "opencv2/highgui/highgui.hpp"#include <iostream>using namespace cv;int main(void){   double alpha, beta, input;   Mat src1, src2, dst;   /// 让用户输入 alpha 值   std::cout << " 简易线性混合(Linear Blender) " << std::endl;   std::cout << "-----------------------" << std::endl;   std::cout << "* 输入 0 到 1 的 alpha 值: ";   std::cin >> input;   // 确认 alpha 值数入的正确在于 0 与 1 之间   if (alpha >= 0 && alpha <= 1)   {    alpha = input;   }   /// 读取两个大小与类型相同的图文件   src1 = imread("C:\\images\\LinuxLogo.jpg");   src2 = imread("C:\\images\\WindowsLogo.jpg");   if (!src1.data)   { std::cout << "读取 src1 错误" << std::endl; return -1; }   if (!src2.data)   { std::cout << "读取 src2 错误" << std::endl; return -1; }   namedWindow("Linear Blend", 1);   beta = (1.0 - alpha);   addWeighted(src1, alpha, src2, beta, 0.0, dst);   imshow("Linear Blend", dst);   waitKey(0);   return 0;}

程序说明

addWeighted(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1):以权重将两图合并

(1)src1:要相加的第一个图文件。

(2)alpha:第一个图文件的权重。

(3)src2:要相加的第二个图文件,因为只是单纯的与第一个图文件相加,所以大小与通道数要与第一个图文件相同。

(4)beta:第二个图文件的权重。

(5)gamma:两图相加后要再增加的值。

(6)dst:两图相加结果的图文件。

(7)dtype:相加结果图的景深(depth);此参数可有可无。

本程序是使用addWeighted函数实现下列公式的图像处理:

g(x)=(1−α)f0(x)+α f1(x);

式中,f0(x)是第一个图;f1(x)是第二个图,因为addWeighted在OpenCV内部就是dst=α·src1+β·src2+γ,只是公式中的γ 为0。

输入分别为0、0.5和1,对应的执行结果如图2-16所示。

图2-16

读者如果使用过Photoshop的图层,应该对此功能不陌生。

注意

虽然alpha与beta是两张图的权重,但其实可以是各小于1的值,而不用相加等于1。

商标特效

商标特效的代码如下:

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include "opencv2/imgproc/imgproc.hpp"#include <vector>using namespace cv;int main(){   // 载入图文件   Mat image1 = imread("C:\\images\\lena.jpg");   Mat logo =          imread("C:\\opencv\\build\\doc\\opencv-logo-white.png");   // 编译器要求使用前要给初始值   Mat image = image1, opencvlogo;   // 缩小原图成 Size(col, row)   resize(logo, opencvlogo, Size(80, 64));   namedWindow("Image 1", CV_WINDOW_AUTOSIZE);   // 定义图有兴趣的区域(Region Of Interest, ROI)   Mat imageROI;   // 指定商标在原图的位置,Rect(x, y, width(col), height(row))   imageROI = image(Rect(420, 420, 80, 64));   imshow("Image 1", opencvlogo);   // 加入商标   addWeighted(imageROI, 1.0, opencvlogo, 0.3, 0., imageROI);   // 显示结果   namedWindow("with logo");   imshow("with logo", image);   waitKey();   return 0;}

程序说明

1.resize(InputArray src, OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR ):改变图像大小

(1)src:输入图像。

(2)dst:输出图像。

(3)dsize:输出图像的图像大小,如果等于0就用下列公式计算。

dsize = Size(round(fxsrc.cols), round(fysrc.rows),但是fx与fy都不可等于0。

(4)fx:水平轴的缩放因子,如果等于0就用下列公式计算:

(double)dsize.width/src.cols。

(5)fy:垂直轴的缩放因子,如果等于0就用下列公式计算:

(double)dsize.height/src.rows。

(6)interpolation:插值方式。

  • INTER_NEAREST:最靠近周围插值法(nearest-neighbor)。

  • INTER_LINEAR:双线性插值法(bilinear)。

  • INTER_AREA:用像素关系区再取样插值法(resampling)。

  • INTER_CUBIC:在4×4像素附近用双立方插值法(bicubic)。

  • INTER_LANCZOS4:在8×8像素附近用Lanczos插值法。

2.size Mat::size() const:返回矩阵大小

size(cols, rows)也返回矩阵大小,同时指定矩阵的行列数。

执行结果如图2-17所示。

图2-17

addWeighted(src1, 0.7, src2, 0.9, 0.0, dst)又可以改写成如下的代码,根据读者喜好自行决定。

src1 = src1 * 0.7;
src2 = src2 * 0.9;
add(src1, src2, dst);

也可简单写成dst = src1 0.7 + src2 0.9,只是Add函数还有其他的参数可以使用。

雨天特效

雨天特效的代码如下:

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include "opencv2/imgproc/imgproc.hpp"#include <vector>using namespace cv;int main(){   Mat image1, image2, image3;   image1 = imread("C:\\images\\lake.jpg");   if (!image1.data)     return 0;   image2= imread("C:\\images\\fur.jpg");   if (!image2.data)     return 0;   // 以image2图像大小调整image1图像大小   resize(image2, image3, image1.size());   //显示原图   namedWindow("Image 1");   imshow("Image 1",image1);   namedWindow("Image 3");   imshow("Image 3",image3);   // 雨天特效图   Mat result;   image3 = image3 * 0.3;   image1 = image1 * 0.9;   add(image1, image3, result);   namedWindow("result");   imshow("result",result);   waitKey();   return 0;}

程序说明

add(InputArray src1, InputArray src2, OutputArray dst, InputArray mask=noArray(), int dtype=-1):计算两图像或颜色值相加

(1)src1:第一个图像或颜色值。

(2)src2:第二个图像或颜色值,与第一个图像同大小。

(3)dst:输出图像。

(4)mask:掩码(mask),可有可无。

(5)dtype:输出图像的景深,可有可无。

湖的原图和特效图如图2-18和图2-19所示。

图2-18

图2-19

雨天特效结果如图2-20所示。

特效图其实是从熊身上采集来的图,如图2-21所示。

图2-20

图2-21

本节程序是介绍图文件像素(pixel)的存储与处理。在说明程序之前,我们先介绍计算机系统中色彩的概念。像素是由颜色空间(color space)或通道(channel)与数据类型(data type)来描述的。对于最简单的黑白图像,我们调整颜色空间与数据类型就可以产生各种灰度的灰色图。

OpenCV像素的数据类型是以下列方式表达的:CV_ABCD。

  • A:每个像素多少位。

  • B:是否有正负号。

  • C:类型前置码。

  • D:通道数目。

例如,CV_8UC3表示如下。

A:每个像素为8位。

B:没有正负号。

C:因为8位没有正负号,所以使用 Char 来表示像素。

D:每个像素有3个通道。

RGB三原色使用CV_8UC3表示如下。

(255, 0, 0):红色。

(0, 255, 0):绿色。

(0, 0, 255):蓝色。

(0, 0, 0):黑色。

(255, 255, 255):白色。

所谓彩色就有很多方式的应用,利用各种的组合让颜色更多样化。一般色彩系统分为以下4种方式。

  • RGB:它是最普遍也是与肉眼最接近的,由红(R)、绿(G)、蓝(B)三色组成,屏幕显示都是用此系统,但注意通道的顺序是RGB。

  • HSV 与 HLS:系统将颜色分为色调(hue)、饱和度(saturation)和明亮度(luminance),是最自然说明色彩的方式。

  • YCrCb:主要用于JPEG图像。

  • CIE Lab*:这是概念上单一的色彩空间,用来计算色距(distance of color)最为方便。

扫描图文件快慢的比较

一般图都是R、G、B三原色。如果颜色太多就会造成图文件非常大,所以为了降低图文件大小都会采用褪色(color reduction)。褪色的目的是加快图文件的扫描进行图像处理,还要加快处理的速度。读者可参考C:\OpenCV\sources\samples\cpp\tutorial_code\core\how_to_scan_images的范例来了解该用法快慢的差异。

OpenCV的遮罩处理(mask operation)

这是根据遮罩值重算图像中的每个像素。例如图像处理方式的公式如下:

I(ij) = 5×I(ij)−[ I(i−1, j) + I(i+1, j) + I(ij−1) + I(ij+1)]

而遮罩的方式则是:

OpenCV提供filter2D函数来进行遮罩处理。

读者可以参考C:\OpenCV\sources\samples\cpp\tutorial_code\core\mat_mask _operations的范例程序来了解如何进行遮罩处理。

2.4 改变对比与明亮度

改变对比与明亮度的代码如下:

#include <opencv/cv.h>#include <opencv/highgui.h>#include <iostream>using namespace cv;double alpha; // 对比控制int beta;   // 明亮度控制int main(int argc, char** argv){   Mat image = imread("C:\\images\\lena.jpg");   Mat new_image = Mat::zeros(image.size(), image.type());   /// Initialize values (Basic Linear Transform)   std::cout << " 基本线性转换" << std::endl;   std::cout << "-------------------------" << std::endl;   std::cout << "* 输入 alpha 值 [1.0-3.0]: "; std::cin >> alpha;   std::cout << "* 输入 beta 值 [0-100]: "; std::cin >> beta;   /// 转换公式 new_image(i,j) = alpha*image(i,j) + beta   for (int y = 0; y < image.rows; y++)   {      for (int x = 0; x < image.cols; x++)      {         for (int c = 0; c < 3; c++)         {            // 针对像素的每个通道做转换            new_image.at<Vec3b>(y, x)[c] =               saturate_cast<uchar>(alpha*(image.at<Vec3b>(y, x)[c]) + beta);         }      }   }   namedWindow("Original Image", 1);   namedWindow("New Image", 1);   imshow("Original Image", image);   imshow("New Image", new_image);   waitKey();   return 0;}

程序说明

图像转换可以使用两种方式:

  • 点(point)的处理。

  • 区块(Neighborhood area-based)处理。

明亮与对比的调整就属于点的处理,而点的处理最普遍就是加乘处理。

g(x)=αf(x)+β

Mat new_image = Mat::zeros( image.size(), image.type() );

为了不影响原图,先建立一个大小与原图相同的矩阵,内容先补0。

new_image.at<Vec3b>(y,x)[c] =
saturate_cast<uchar>( alpha*( image.at<Vec3b>(y,x)[c] ) + beta );

图像处理使用3个for循环处理图像的每个点,第一个是行、第二个是列、第三个是通道。因为原图是彩色的,所以处理3个通道(R,G,B)需要3个循环。由于是对图像的每一个点进行处理,所以时间就会久一点。

在图像点的指定是使用image.at<Vec3b>(y,x)[c],y是列、x是行,c就是R、G、B(0、1、2),R = 0,以此类推。

这3层循环程序也可以简化成为如下单一指令。

image.convertTo(new_image,-1, alpha, beta);

执行结果alpha = 2.2,beta = 50,如图2-22所示。

图2-22



本文摘自图书《OpenCV和Visual Studio图像识别应用开发》

点击阅读原文试读更多章节

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存